﻿using System;
using System.Diagnostics.Contracts;
using System.Windows.Controls;
using System.Windows.Input;

namespace Cloak.Mvvm.Navigation
{
	/// <summary>
	/// Represents a view which hosts a navigation frame
	/// </summary>
	public class NavigationModel : ViewModel, INavigationContext
	{
		private readonly IPageModelMap _pageModelMap;
		private readonly IPageModelScope _rootPageModelScope;
		private Uri _pageUri;
		private IPageModelScope _pageModelScope;

		/// <summary>
		/// Initializes an instance of <see cref="NavigationModel"/>
		/// </summary>
		/// <param name="pageModelMap">The map which determines view models for page types</param>
		/// <param name="rootPageModelScope">The root scope in which view models are instantiated for pages</param>
		public NavigationModel(IPageModelMap pageModelMap, IPageModelScope rootPageModelScope)
		{
			Contract.Requires(pageModelMap != null);
			Contract.Requires(rootPageModelScope != null);

			_pageModelMap = pageModelMap;
			_rootPageModelScope = rootPageModelScope;

			NavigatedCommand = new MethodCommand<NavigatedPage>(OnNavigated);
		}

		#region INavigationContext
		/// <summary>
		/// Navigates this context to the resource represented by the specified URI
		/// </summary>
		/// <param name="pageUri">The URI of the resource to load in this context</param>
		public void Navigate(Uri pageUri)
		{
			PageUri = pageUri;
		}
		#endregion

		/// <summary>
		/// Gets the URI of the current page
		/// </summary>
		public Uri PageUri
		{
			get { return _pageUri; }
			private set { SetProperty("PageUri", ref _pageUri, value); }
		}

		/// <summary>
		/// Gets the command which indicates the frame navigated to a new page
		/// </summary>
		public ICommand NavigatedCommand { get; private set; }

		private void OnNavigated(NavigatedPage newPage)
		{
			Contract.Requires(newPage != null);

			// This ensures we have the most up-to-date URI if the view navigates on its own, such as when the
			// user clicks the back or forward buttons. Raising the PropertyChanged event here would result in
			// the client navigating again, which would invoke this method again, and so on, causing an infinite loop.
			// To avoid that, we assign the field directly instead of the property.
			_pageUri = newPage.Uri;

			BeginPageModelScope(newPage);
		}

		private void BeginPageModelScope(NavigatedPage newPage)
		{
			DisposePageModelScope();

			SetPageModel(newPage);
		}

		private void DisposePageModelScope()
		{
			if(_pageModelScope != null)
			{
				_pageModelScope.Dispose();
				_pageModelScope = null;
			}
		}

		private void SetPageModel(NavigatedPage newPage)
		{
			_pageModelScope = _rootPageModelScope.BeginScope();

			var model = ResolvePageModel(newPage);

			if(model != null)
			{
				newPage.SetModel(model);
			}
		}

		private ViewModel ResolvePageModel(NavigatedPage newPage)
		{
			var reference = _pageModelMap.GetPageModelReference(newPage.Type);

			return reference == null ? null : _pageModelScope.ResolvePageModel(newPage.Uri, reference, newPage.Parameters);
		}
	}
}